/*
 * DG (DG Gaming Engine)  - API
 * Copyright (C) 2004  Bob Marks (marksie531@yahoo.com)
 * http://code.google.com/p/dng-gaming
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
package com.dg.gaming.client.gui;

import java.awt.*;
import java.awt.event.*;
import java.util.*;

import javax.swing.*;

import info.clearthought.layout.TableLayout;
import nanoxml.XMLElement;
import com.dg.gaming.client.*;
import com.dg.gaming.api.common.*;
import com.dg.gaming.api.common.msg.*;
import com.dg.gaming.api.common.util.*;

/**
 * <p>The important DGTableFrame which is where each game is played.  Each time
 * a new table is created one of these frames appear.  This class creates the
 * buttons at the top of the screen, the player list at the bottom left and the
 * message chat box on the bottom right. The main game area is set in the sub
 * classes e.g. ChessTableFrame using the setGamePanel (JPanel) method.</p>
 *
 * <p>To use this class a class must extend it e.g. ChesTableFrame.  Its fields
 * must include a data model (GameModel e.g. ChessModel), a view of the data
 * (DGComponent e.g. ChessBoardComponent) and a controller (DGController
 * e.g. ChessController) for understanding mouse movements etc on the view.</p>
 *
 * <p>The constructor in the sub class should initialise these MVC fields and
 * MUST call the setMVC (model, view, controller) method as this class
 * makes use of these.</p>
 *
 * @author  Bob Marks
 * @author  Garrett Lehman (Gman)
 * @version Alpha 0.2.3
 */
public abstract class DGTableFrame extends JFrame implements ITable,
        Observer {

    // Declare constants
    private static final int WIDTH = 600;
    private static final int HEIGHT = 500;
    private static final int SPACING = 5;
    private static final Font GAME_OVER_FONT = new Font("Arial", Font.BOLD, 20);
    private static final Color GAME_OVER_COLOUR = new Color(200, 0, 0);

    private String title; // title of this frame
    protected long username; // Username from conn.getUsername();
    protected TableConnectionThread conn = null; // Connection
    protected String tableNum; // number of this table
    protected GameModel gameModel = null; // Client gameModel
    protected GameController gameController = null; // GameController
    protected DGComponent gameMainView = null; // main view
    protected Table table = null;
    protected Player player = null;

    // Link to table hash in DGClientFrame
    private HashMap tableFramesHash = null;

    // Declare GUI components
    private DGGlassPane glassPane;
    private DGPanel gamePanel, topPanel;
    protected ChatTableComponent messageComponent;
    protected DGButton sitButton, standButton, startButton;
    protected DGButton offerDrawButton, resignButton, inviteButton;
    private JAvailableSeats availablePlayers;
    private JTablePlayers tablePlayers;

    /**
     * Constructor to a standard DGGameFrame.  NOTE: Every contructor of a
     * sub class of this must call the setMVC (model, view, controller) method.
     *
     * @param conn	 Client connection thread which holds the link to the
     *               server, user and table lists.
     * @param width  Width of the table frame.
     * @param height Height of the table frame.
     */
    public DGTableFrame(TableConnectionThread conn, int width, int height) {
        // Set fields
        this.conn = conn;

        // Set convience fields
        this.tableNum = conn.getTableNum();
        this.table = conn.getTable();
        this.username = conn.getUsername();
        this.player = this.table.getPlayerList().getPlayer(username);

        // Set title
        this.title = GameLabels.getTableTitle() + " " +
                     tableNum + " - " +
                     DGLabels.getInstance().get("player") + ": " +
                     conn.getUsername();

        // Set title of frame
        setTitle(title);

        // Set up GUI
        setUpGUI();

        // Set image icon
        setIconImage(GameImages.getImage("game.icon"));

        // Add listeners
        addListeners();

        // Set size of the frame
        if (width != -1 && height != -1) {
            setSize(new Dimension(width, height));
        }
    }

    /**
     * Constructor which doesn't take a width and height.
     *
     * @param conn   Table connection thread.
     */
    public DGTableFrame(TableConnectionThread conn) {
        this(conn, -1, -1);
    }

    /**
     * Set up the standard GUI elements
     */
    private void setUpGUI() {

        // Retrieve labels resources
        DGLabels resources = DGLabels.getInstance();

        // Set background colour
        getContentPane().setBackground(GameProperties.getBackgroundColour());

        // Create glass pane
        this.glassPane = new DGGlassPane();
        getRootPane().setGlassPane(glassPane);

        // Create main panel
        double pref = TableLayout.PREFERRED, fill = TableLayout.FILL;
        double[][] sizes = { {fill}, {50, pref, fill, pref}
        };
        DGPanel mainPanel = new DGPanel(sizes);
        mainPanel.setBorder(BorderFactory.createEtchedBorder());

        // Create top panel
        sizes = new double[][] { {pref, SPACING, pref}, {pref}
        };
        topPanel = new DGPanel(sizes);

        // Create game panel
        gamePanel = new DGPanel(new BorderLayout());

        // Create bottom panel (users cannot add to this).
        sizes = new double[][] { {0.3, 0.7}, {100}
        };
        DGPanel bottomPanel = new DGPanel(sizes);

        // Create buttons
        sizes = new double[][] { {SPACING, pref, SPACING, pref, SPACING, pref,
                SPACING, pref, SPACING}, {SPACING, pref, SPACING}
        };
        DGPanel firstPanel = new DGPanel(sizes);
        sizes = new double[][] { {SPACING, pref, SPACING, pref, SPACING, pref,
                SPACING, pref, SPACING}, {SPACING, pref, SPACING}
        };
        DGPanel secondPanel = new DGPanel(sizes);
        firstPanel.setBorder(BorderFactory.createEtchedBorder());
        secondPanel.setBorder(BorderFactory.createEtchedBorder());

        // Set up available players component
        availablePlayers = new JAvailableSeats(player, table.getPlayerList(),
                                               table);

        // Set up buttons
        sitButton = new DGButton(resources.get("sit"));
        standButton = new DGButton(resources.get("stand"));
        startButton = new DGButton(resources.get("start"));
        offerDrawButton = new DGButton(resources.get("offer.draw"));
        resignButton = new DGButton(resources.get("resign"));
        inviteButton = new DGButton(resources.get("invite"));

        // Add to subPanels
        firstPanel.add(new JLabel(resources.get("playing.as")), "1,1");
        firstPanel.add(availablePlayers, "3,1");
        firstPanel.add(sitButton, "5,1");
        firstPanel.add(standButton, "7,1");
        secondPanel.add(startButton, "1,1");
        secondPanel.add(offerDrawButton, "3,1");
        secondPanel.add(resignButton, "5,1");
        secondPanel.add(inviteButton, "7,1");

        topPanel.add(firstPanel, "0,0");
        topPanel.add(secondPanel, "2,0");

        // Create message component
        messageComponent = new ChatTableComponent(conn, 3);

        // Create table players
        tablePlayers = new JTablePlayers(table.getPlayerList());
        DGPanel tablePlayersPanel = new DGPanel(new double[][] { {fill}, {fill}
        });
        tablePlayersPanel.setBorder(
                BorderFactory.createTitledBorder(
                        BorderFactory.createEtchedBorder(),
                        DGLabels.getInstance().get("user.list")
                )
                );
        DGScrollPane tablePlayersScroll = new DGScrollPane(tablePlayers);
        tablePlayersPanel.add(tablePlayersScroll, "0,0");

        // Add these components to bottom panel
        bottomPanel.add(tablePlayersPanel, "0,0");
        bottomPanel.add(messageComponent, "1,0");

        // Add to main panel
        mainPanel.add(new DGTitlePanel(), "0,0");
        mainPanel.add(topPanel, "0,1");
        mainPanel.add(gamePanel, "0,2");
        mainPanel.add(bottomPanel, "0,3");

        // Add the main panel to the content pane
        getContentPane().add(mainPanel);

        // Refresh state of buttons
        updateGuiStates();

        Dimension minSize = this.getPreferredSize();
        Dimension size = new Dimension
                         (
                                 Math.max((int) minSize.getWidth(), WIDTH),
                                 Math.max((int) minSize.getHeight(), HEIGHT)
                         );
        setSize(size);
        Dimension screenSize = getToolkit().getScreenSize();
        this.setLocation((int) screenSize.getWidth() / 2 - this.getWidth() / 2,
                         (int) screenSize.getHeight() / 2 -
                         this.getHeight() / 2);
        setVisible(true);
    }

    /**
     * Add listeners
     */
    private void addListeners() {
        //add close listener to frame
        setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
        addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
                leaveTable();
            }
        });

        // Add listener for the sit button
        sitButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                sitDown();
            }
        });

        // Add listener for the stand button
        standButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                standUp();
            }
        });

        // Add listener for
        startButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                readyToStart();
            }
        });

        // Add listener for the offer draw button
        offerDrawButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                offerDraw();
            }
        });

        // Add listener for resign button
        resignButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                resign();
            }
        });

        // Add listener for invite button
        inviteButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                invite();
            }
        });

        // Add listeners on data
        table.addObserver(this); // 1) Table observer
        table.getPlayerList().addObserver(this); // 2) Player list observer
    }

    /**
     * Set the link to the table frames hash in the DGClientFrame (must
     * be deleted when the client is removed).
     *
     * @param tableFramesHash
     */
    public void setTableFramesHash(HashMap tableFramesHash) {
        this.tableFramesHash = tableFramesHash;
    }

    /**
     * Player leaves the table.
     */
    public void leaveTable() {
        // Check that the controller isn't null
        if (gameController != null) {

            // Check to see if the game is playing or not
            if (gameController.isGamePlaying() &&
                gameController.getSeatNum() != Player.NOT_SEATED) {
                if (JOptionPane.showConfirmDialog(
                        this,
                        "You cannot leave a table in progress.  Do you wish to resign?",
                        "Close Table",
                        JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) {
                    // Send resign method to server
                    MsgGameOver commGameOver = new MsgGameOver(IGameOver.
                            USER_RESIGNS);
                    conn.send(commGameOver);

                    // Now close the frame and removed any references to it
                    closeTableFrame();
                }
                return;
            }
        }

        // Now close the frame and removed any references to it
        closeTableFrame();
    }

    /**
     * Close the table frame.
     */
    private void closeTableFrame() {
        String tableNum = table.getTableNum();

        // Remove table from table hash in DGClientFrame
        TableList tableList = conn.getTableList();
        tableFramesHash.remove(new Integer(tableNum));
        tableList.removePlayer(tableNum, username);
        tableList.refreshObservers();

        // Inform other clients at table
        MsgExitTable commExitTable = new MsgExitTable();
        conn.send(commExitTable);

        // Set visible equal to false and dispose this frame
        setVisible(false);
        dispose();
    }

    /**
     * Set the empty panel in the screen equal to the game panel.
     *
     * @param gamePanel
     */
    public void setGamePanel(DGPanel gamePanel) {
        // Set up game panel (user must create this) in the middle
        this.gamePanel.add(gamePanel, BorderLayout.CENTER);
        validate();
        invalidate();
    }

    /**
     * Method to add additional components (such as buttons etc) to the top
     * right hand side of the screen.
     *
     * @param component
     */
    public void addToTopPanel(Component component) {
        topPanel.add(component);
    }

    /**
     * Accessor for the game controller.
     * @return
     */
    public GameController getController() {
        return gameController;
    }

    /**
     * Accessor for the game controller.
     *
     * @return
     */
    public GameModel getModel() {
        return gameModel;
    }

    /**
     * Accessor for the small message box with each table.
     *
     * @return
     */
    public ChatGameComponent getMessageComponent() {
        return messageComponent;
    }

    /**
     * Get invite dialog
     */
    public DGInviteDialog getInviteDialog() {
        return new DGInviteDialog(this,
                                  DGLabels.getInstance().get("invite"), true,
                                  conn);
    }

    /**
     * Return glass pane.
     *
     * @return
     */
    public DGGlassPane getDGGlassPane() {
        return this.glassPane;
    }

    /**
     * Set the DG glass pane.
     *
     * @param glassPane
     */
    public void setDGGlassPane(DGGlassPane glassPane) {
        this.glassPane = glassPane;
    }

    /**
     * Update the states of the buttons (done on a data change).
     */
    private void updateGuiStates() {
        if (table != null && player != null) {
            // Update the actual button
            sitButton.setEnabled(player.canSit(table));
            availablePlayers.setEnabled(player.canSit(table));
            standButton.setEnabled(player.canStand(table));
            startButton.setEnabled(player.canStart(table, conn.getGame()));
            offerDrawButton.setEnabled(player.canOfferDrawResign());
            resignButton.setEnabled(player.canOfferDrawResign());

            DGLabels labels = DGLabels.getInstance();
            if (player.isSeated() && !player.canStart(table, conn.getGame())) {
                glassPane.display(labels.get("waiting.on.other.players.to.sit"));
            } else if (player.canStart(table, conn.getGame())) {
                glassPane.display(labels.get("click.start.to.begin.game"));
            } else if (player.isReady() && !table.isGamePlaying()) {
                glassPane.display(labels.get(
                        "waiting.on.other.players.to.start"));
            } else {
                glassPane.setVisible(false);
            }
        }
    }

    /**
     * Implementation of the ITable interface.
     *
     * @see com.dg.gaming.client.gui.ITable#receiveTableMessage(nanoxml.XMLElement)
     */
    public void receiveTableMessage(XMLElement message, String tableNum) {
        try {
            String messageType = message.getName();

            if (messageType.equals(Msg.START_GAME)) {
                startGame(new MsgStartGame(message));
            } else if (messageType.equals(Msg.GAME_OVER)) {
                gameOver(new MsgGameOver(message));
            } else if (messageType.equals(Msg.OFFER_DRAW)) {
                receiveOfferDraw(new MsgOfferDraw(message));
            } else if (messageType.equals(Msg.CONTROLLER_PROPERTY)) {
                receiveProperty(new MsgControllerProperty(message));
            } else if (messageType.equals(Msg.CONTROLLER_OBJECT)) {
                receiveObject(new MsgControllerObject(message));
            } else if (messageType.equals(Msg.CHAT_TABLE)) {
                MsgChatTable commMessage = new MsgChatTable(message);
                receiveChatTableMessage(commMessage.getUserid(),
                                        commMessage.getTableNum(),
                                        commMessage.getChat());
            } else if (messageType.equals(Msg.JOIN_TABLE)) {
                MsgJoinTable commJoinTable = new MsgJoinTable(message);
                if (commJoinTable.containsModel()) {
                    gameModel.setState(commJoinTable.getModelState());
                }
            }
                    // Also pass message into adapter method which all DG clients
                    // can receive to do additional processing if this is required.
                    receiveMessage(message); {
            }
        } catch (TransmissionException transEx) {}
    }

    /**
     * Adapter method for receiving a custom message.
     *
     * @see com.dg.gaming.client.gui.ITable#receiveTableMessage(nanoxml.XMLElement)
     */
    protected void receiveMessage(XMLElement message) {}

    /**
     * Receive start game message from server.  Change status of players, reset current
     * player (for turn based games), call controller.start() and startGame() on
     * any sub class of this class.
     *
     * @param commStartGame
     */
    private void startGame(MsgStartGame commStartGame) {
        // Start controller
        this.gameController.start(); // Controller is also informed game is started

        // Delegate to custom startGame () method in client if required
        this.startGame();
    }

    /**
     * Start the game (Empty adapter method).
     */
    public void startGame() {}

    /**
     * This client wishes to sit down at a particular table.
     */
    private void sitDown() {
        int selectedSeat = availablePlayers.getSelectedSeat();

        // Create table action communications object and send to server
        MsgSitDown commSitDown = new MsgSitDown(selectedSeat);
        conn.send(commSitDown);
    }

    /**
     * Stand back up again (status of player becomes VIEWING).
     */
    private void standUp() {
        // Set the status of the player back to VIEWING.
        MsgStandUp commStandUp = new MsgStandUp();

        conn.send(commStandUp);
    }



    /**
     * Player clicks the start button.
     */
    private void readyToStart() {
        MsgReadyToStart commReadyToStart = new MsgReadyToStart();
        conn.send(commReadyToStart);
    }

    /**
     * Offer the user a draw.
     */
    private void offerDraw() {
        // Create offer draw communications object and send to server.
        MsgOfferDraw commOfferDraw = new MsgOfferDraw(
                MsgInvite.REQUEST);
        conn.send(commOfferDraw);
    }

    /**
     * Receive offer draw.
     *
     * @param offerDraw
     */
    private void receiveOfferDraw(MsgOfferDraw offerDraw) {
        DGLabels dgLabels = DGLabels.getInstance();

        // Check to see if this is an offer draw or an offer draw reply
        if (offerDraw.getStatus() == MsgInvite.REQUEST) {

            String message =
                    dgLabels.get("player") + " " + offerDraw.getUserid() +
                    " " +
                    dgLabels.get("offers.a.draw") + " " +
                    dgLabels.get("accept") + "?";

            int answer = JOptionPane.showConfirmDialog(
                    this,
                    message,
                    dgLabels.get("table.message"),
                    JOptionPane.YES_NO_OPTION
                         );

            // Convert to a number the CommInvite object understands
            int yesNo = (answer == JOptionPane.YES_OPTION) ?
                        MsgInvite.ACCEPT : MsgInvite.DECLINE;

            // Create invite comm object to reply to the users
            MsgOfferDraw offerDrawReply = new MsgOfferDraw(
                    yesNo, offerDraw.getUserid());
            offerDrawReply.setSerialNum(offerDraw.getSerialNum());

            // Transmit message back to the server
            conn.send(offerDrawReply);
        } else { // is an offer draw reply from a user
            if (offerDraw.getStatus() == MsgInvite.DECLINE) {

                // report message that user has declined the offer
                String messageTitle =
                        "User " + offerDraw.getUserid() + " " +
                        dgLabels.get("has.rejected.draw.offer") + ".";

                JOptionPane.showMessageDialog(
                        this,
                        messageTitle,
                        dgLabels.get("table.message"),
                        JOptionPane.INFORMATION_MESSAGE
                        );
            }
        }
    }

    /**
     * Resign from this particular game.
     */
    private void resign() {
        // Player resigns
        MsgGameOver commGameOver = new MsgGameOver
                                   (IGameOver.USER_RESIGNS);
        conn.send(commGameOver);
    }

    /**
     * Pop up invite dialog and invite someone to the table
     */
    private void invite() {
        this.getInviteDialog();
    }

    /**
     * Sets up the MVC for this table.  This method <b>must</b> be called from
     * the constructor of any sub class of this class as this class refers to
     * the GameModel and DGController.
     *
     * @param model
     * @param mainView
     * @param controller
     */
    public void setupMVC(GameModel model, DGComponent mainView,
                         GameController controller) {
        this.gameModel = model;
        this.gameMainView = mainView;
        this.gameController = controller;
    }

    /**
     * Create game over message and add to chat box.
     *
     * @param commGameOver
     */
    private void gameOver(MsgGameOver commGameOver) {
        // Create game over message
        DGLabels labels = DGLabels.getInstance();
        StringBuffer fromSB, ratingsSB;

        // Depending on the status create the game over message
        GameOver gameOver = commGameOver.getGameOver();
        long[] players = gameOver.getPlayers();
        int[] results = gameOver.getResults();
        int[] oldRatings = gameOver.getOldRatings();
        int[] newRatings = gameOver.getNewRatings();

        // Loop through the players
        for (int i = 0; i < players.length; i++) {
            int result = results[i];

            // Create from message
            fromSB = new StringBuffer(
                    labels.get("player") + " " + players[i] + " ");
            if (result == IGameOver.DRAW) {
                fromSB.append(labels.get("draws"));
            } else if (result == IGameOver.LOSE) {
                fromSB.append(labels.get("loses"));
            } else if (result == IGameOver.WIN) {
                fromSB.append(labels.get("wins"));
            }

            // Create old / new ratings
            ratingsSB = new StringBuffer(
                    labels.get("old.rating") + " " + oldRatings[i] + " " +
                    labels.get("new.rating") + " " + newRatings[i]);

            // Write to the message component
            messageComponent.receiveMessage(fromSB.toString(),
                                            ratingsSB.toString());
        }

        // Display "Game Over" text in glass pane
        StringBuffer gameOverSB = new StringBuffer(labels.get("game.over"));
        int seatNum = player.getSeatNum();
        if (seatNum >= 0 && seatNum < players.length) {
            if (results[seatNum] == IGameOver.DRAW) {
                gameOverSB.append(" - " + labels.get("draw"));
            } else if (results[seatNum] == IGameOver.LOSE) {
                gameOverSB.append(" - " + labels.get("you.lose"));
            } else if (results[seatNum] == IGameOver.WIN) {
                gameOverSB.append(" - " + labels.get("you.win"));
            }
        }
        glassPane.display(GAME_OVER_FONT, GAME_OVER_COLOUR, gameOverSB.toString());
    }

    /**
     * Receive a property.
     *
     * @param commProperty
     */
    private void receiveProperty(MsgControllerProperty commProperty) {
        // retrieve controller from the correct table.
        GameController controller = getController();

        switch (commProperty.getStatus()) {
        case MsgControllerProperty.TYPE_STRING:
            controller.receiveProperty(commProperty.getKey(),
                                       commProperty.getValue());
            return;

        case MsgControllerProperty.TYPE_INT:
            int value = Integer.parseInt(commProperty.getValue());
            controller.receiveProperty(commProperty.getKey(), value);
            return;

        case MsgControllerProperty.TYPE_INT_TWO:
            StringTokenizer st = new StringTokenizer(commProperty.getValue());
            int value1 = Integer.parseInt(st.nextToken());
            int value2 = Integer.parseInt(st.nextToken());
            controller.receiveProperty(commProperty.getKey(), value1, value2);
            return;
        }
    }

    /**
     * Receive a message for a specificied table.
     *
     *
     * @param usernameFrom
     * @param tableNum
     * @param messageText
     */
    private void receiveChatTableMessage(long usernameFrom, String tableNum,
                                         String messageText) {
        getMessageComponent().receiveMessage(usernameFrom + "", messageText);
    }

    /**
     * Retrieve controller object.
     *
     * @param commObject
     */
    private void receiveObject(MsgControllerObject commObject) {
        GameController controller = getController();
        controller.receiveObject(commObject.getData());
    }

    /**
     * Update and refresh the buttons when the data changes.
     *
     * @see java.util.Observer#update(java.util.Observable, java.lang.Object)
     */
    public void update(Observable obs, Object obj) {
        updateGuiStates();
    }
}
